package com.suyeer.fastwechat.util;

import com.alibaba.fastjson.JSONObject;
import com.suyeer.basic.util.HttpResUtil;
import com.suyeer.basic.util.JsonUtil;
import com.suyeer.basic.util.LogUtil;
import com.suyeer.cache.MemCachedUtil;
import com.suyeer.cache.RedisUtil;
import com.suyeer.fastwechat.bean.fwcommon.CacheBean;
import com.suyeer.fastwechat.bean.fwcommon.JsSdkConfig;
import com.suyeer.fastwechat.enums.CacheLocationEnum;
import com.suyeer.fastwechat.module.WeChatDataCache;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.time.DateUtils;
import redis.clients.jedis.Jedis;

import javax.servlet.http.HttpServletRequest;
import java.util.Date;
import java.util.TreeMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.atomic.AtomicBoolean;

import static com.suyeer.fastwechat.util.FwConstUtil.WX_PARAM_ACCESS_TOKEN;
import static com.suyeer.fastwechat.util.FwConstUtil.WX_PARAM_TICKET;
import static java.util.concurrent.Executors.newFixedThreadPool;

/**
 * @author jun 2018/11/22
 */
public class FwTokenUtil {
    private final static int CACHE_TIME = FwConstUtil.ACCESS_TOKEN_ACTIVE_TIME;
    private final static int TWO_HOURS_SECOND = 7200;
    private final static int ONE_MIN = 1000 * 60;
    private final static int MAX_ERROR_TIME = 3;
    private final static AtomicBoolean IF_RUN = new AtomicBoolean(false);
    private final static ExecutorService EXECUTOR_SERVICE = newFixedThreadPool(10);

    public static String getAccessToken() {
        if (FwConstUtil.FW_CACHE_LOCATION == CacheLocationEnum.MEMCACHED) {
            return getAccessTokenFromMC();
        }
        if (FwConstUtil.FW_CACHE_LOCATION == CacheLocationEnum.REDIS) {
            return getAccessTokenFromRedis();
        }
        if (FwConstUtil.FW_CACHE_LOCATION == CacheLocationEnum.LOCALHOST) {
            return getAccessTokenFromLocal();
        }
        if (FwConstUtil.FW_CACHE_LOCATION == CacheLocationEnum.UDP) {
            return FwUdpUtil.udpClient(WX_PARAM_ACCESS_TOKEN);
        }
        return getAccessTokenFromWx();
    }

    public static String getJsApiTicket() {
        if (FwConstUtil.FW_CACHE_LOCATION != CacheLocationEnum.NO_CACHE) {
            return getJsApiTicketFromLocal();
        }
        return getJsApiTicketFromWx();
    }

    private static String getAccessTokenFromWx() {
        try {
            return getFromWx(FwConstUtil.WX_URL_CGI_BIN_ACCESS_TOKEN, WX_PARAM_ACCESS_TOKEN);
        } catch (Exception e) {
            LogUtil.error("{} 获取AccessToken失败, {}", FwConstUtil.ERROR, e.getMessage());
            return null;
        }
    }

    /**
     * 从微信端获取JsApiTicket, 如果是UDP模式, 则从上游UDP服务器获取
     * @return String
     */
    private static String getJsApiTicketFromWx() {
        try {
            if (FwConstUtil.FW_CACHE_LOCATION == CacheLocationEnum.UDP) {
                return FwUdpUtil.udpClient(WX_PARAM_TICKET);
            }
            return getFromWx(FwConstUtil.WX_URL_GET_JS_API_TICKET + getAccessToken(), WX_PARAM_TICKET);
        } catch (Exception e) {
            LogUtil.error("{} 获取JsApiTicket失败, {}", FwConstUtil.ERROR, e.getMessage());
            return null;
        }
    }

    /**
     * 本地AccessToken缓存时间默认设为6000秒, 超过这个时间后, 则请求微信生成新的AccessToken数据;
     * 缓存时间可自主修改参数 accessTokenActiveTime
     *
     * @return String
     */
    private static String getAccessTokenFromLocal() {
        try {
            CacheBean cache = WeChatDataCache.getInstance().get(FwConstUtil.WX_KEY_ACCESS_TOKEN);
            if (cache != null && cache.isNotExpired()) {
                return cache.getValue();
            }
            String value = getAccessTokenFromWx();
            if (value == null) {
                throw new Exception("与微信服务器交互异常!");
            }
            WeChatDataCache.getInstance().add(FwConstUtil.WX_KEY_ACCESS_TOKEN, value, CACHE_TIME);
            return value;
        } catch (Exception e) {
            LogUtil.error("{} 获取AccessToken失败, {}", FwConstUtil.ERROR, e.getMessage());
            return null;
        }
    }

    /**
     * JsApiTicket 在微信生成规则是, 从 00:00 开始, 每隔 2 小时更新一次.
     * 所以此数据只保存本地即可. 无需同步到 memcached.
     *
     * @return String
     */
    private static String getJsApiTicketFromLocal() {
        try {
            CacheBean cache = WeChatDataCache.getInstance().get(FwConstUtil.WX_KEY_JS_API_TICKET);
            if (cache != null && cache.isNotExpired()) {
                return cache.getValue();
            }
            String value = getJsApiTicketFromWx();
            if (value == null) {
                throw new Exception("与微信服务器交互异常!");
            }
            newThread4UpdateJsApiTicketCache(value);
            return value;
        } catch (Exception e) {
            LogUtil.error("{} 获取JsApiTicket失败, {}", FwConstUtil.ERROR, e.getMessage());
            return null;
        }
    }

    private static void newThread4UpdateJsApiTicketCache(String value) {
        if (IF_RUN.compareAndSet(false, true)) {
            EXECUTOR_SERVICE.execute(() -> {
                refreshJsApiTicketFromWx(value, 0);
                IF_RUN.set(false);
            });
        }
    }

    private static void refreshJsApiTicketFromWx(String value, int errorTime) {
        try {
            if (errorTime < MAX_ERROR_TIME) {
                Long nowSec = FwBaseUtil.getTimestamp();
                int overSec = nowSec.intValue() % TWO_HOURS_SECOND;
                int remainderSec = TWO_HOURS_SECOND - overSec;
                boolean flag = (remainderSec < FwConstUtil.TICKET_CACHE_REFRESH_TIME || overSec < FwConstUtil.TICKET_CACHE_REFRESH_TIME);
                int cacheTime = flag ? FwConstUtil.TICKET_CACHE_REFRESH_TIME : remainderSec - FwConstUtil.TICKET_CACHE_REFRESH_TIME;
                if (value != null) {
                    WeChatDataCache.getInstance().add(FwConstUtil.WX_KEY_JS_API_TICKET, value, cacheTime);
                }
                if (flag) {
                    Thread.sleep(ONE_MIN);
                    value = getJsApiTicketFromWx();
                    if (value == null) {
                        throw new Exception("与微信服务器交互异常!");
                    }
                    refreshJsApiTicketFromWx(value, errorTime);
                }
            }
        } catch (Exception e) {
            errorTime++;
            LogUtil.error("{} 第 {} 次执行 refreshJsApiTicketFromWx 失败, {}", FwConstUtil.ERROR, errorTime, e.getMessage());
            refreshJsApiTicketFromWx(value, errorTime);
        }
    }

    /**
     * memcached AccessToken缓存时间默认设为6000秒, 超过这个时间后, 则请求微信生成新的AccessToken数据;
     * 缓存时间可自主修改参数 accessTokenActiveTime
     *
     * @return String
     */
    private static String getAccessTokenFromMC() {
        try {
            String value = (String) MemCachedUtil.getMemCachedClient().get(FwConstUtil.KEY_CACHE_WX_ACCESS_TOKEN);
            if (value != null) {
                return value;
            }
            value = getAccessTokenFromWx();
            if (value == null) {
                throw new Exception("与微信服务器交互异常!");
            }
            MemCachedUtil.getMemCachedClient().set(FwConstUtil.KEY_CACHE_WX_ACCESS_TOKEN, value, DateUtils.addSeconds(new Date(), CACHE_TIME));
            return value;
        } catch (Exception e) {
            LogUtil.error("{} 获取AccessToken失败, {}", FwConstUtil.ERROR, e.getMessage());
            return null;
        }
    }

    /**
     * redis AccessToken缓存时间默认设为6000秒, 超过这个时间后, 则请求微信生成新的AccessToken数据;
     * 缓存时间可自主修改参数 accessTokenActiveTime
     * 无需调用 CacheBean 的 isNotExpired 检查;
     *
     * @return String
     */
    private static String getAccessTokenFromRedis() {
        try {
            Jedis jedis = RedisUtil.getJedis();
            String value = jedis.get(FwConstUtil.KEY_CACHE_WX_ACCESS_TOKEN);
            RedisUtil.close(jedis);
            if (value != null) {
                return value;
            }
            value = getAccessTokenFromWx();
            if (value == null) {
                throw new Exception("与微信服务器交互异常!");
            }
            jedis = RedisUtil.getJedis();
            jedis.set(FwConstUtil.KEY_CACHE_WX_ACCESS_TOKEN, value);
            jedis.expire(FwConstUtil.KEY_CACHE_WX_ACCESS_TOKEN, CACHE_TIME);
            RedisUtil.close(jedis);
            return value;
        } catch (Exception e) {
            LogUtil.error("{} 获取AccessToken失败, {}", FwConstUtil.ERROR, e.getMessage());
            return null;
        }
    }

    private static String getFromWx(String url, String key) throws Exception {
        JSONObject obj = HttpResUtil.sendHttpGetRequest(url);
        if (obj == null || !obj.containsKey(key)) {
            throw new Exception(String.format("微信返回值错误: %s, 请求URL: %s", JsonUtil.toString(obj), url));
        }
        return obj.getString(key);
    }

    public static JSONObject getJsSdkConfig(String url) {
        JSONObject retObj = new JSONObject();
        try {
            JsSdkConfig jsSdkConfig = new JsSdkConfig();
            TreeMap<String, String> treeMap = new TreeMap<>();
            treeMap.put("noncestr", jsSdkConfig.getNonceStr());
            treeMap.put("jsapi_ticket", getJsApiTicket());
            treeMap.put("timestamp", jsSdkConfig.getTimestamp());
            treeMap.put("url", url);
            jsSdkConfig.setSignature(FwBaseUtil.createJsSdKSign(treeMap));
            retObj = JsonUtil.changeType(jsSdkConfig, JSONObject.class);
        } catch (Exception e) {
            LogUtil.error("{} 获取JsSdk的配置信息JsSdkConfig失败：{}", FwConstUtil.ERROR, e.getMessage());
        }
        return retObj;
    }

    public static JSONObject getJsSdkConfig(HttpServletRequest request) {
        String reqQuery = StringUtils.isNotBlank(request.getQueryString()) ? "?" + request.getQueryString() : "";
        String url;
        if (StringUtils.isNotBlank(FwConstUtil.FW_DOMAIN)) {
            url = FwConstUtil.FW_DOMAIN + request.getRequestURI() + reqQuery;
        } else {
            url = request.getRequestURL().toString() + reqQuery;
        }
        return getJsSdkConfig(url);
    }

}
